﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Microsoft.Xna.Framework;
using System.Reflection;
using System.Diagnostics;

namespace Skugo.Shared
{
    public static class TypeFactory
    {
        /// <summary>
        /// Maintain a map of type hashes to their corresponding type.
        /// </summary>
        private static Dictionary<Int64, PrimitiveTypeInfo> GuidToPrimitiveInfo = new Dictionary<Int64, PrimitiveTypeInfo>();

        /// <summary>
        /// A map of all types in the factory to their corresponding guid.
        /// </summary>
        private static Dictionary<Type, PrimitiveTypeInfo> TypeToPrimitiveInfo = new Dictionary<Type, PrimitiveTypeInfo>();

        /// <summary>
        /// A map of all types in the factory to their corresponding guid.
        /// </summary>
        private static Dictionary<Type, EnumerableTypeInfo> TypeToEnumerableInfo = new Dictionary<Type, EnumerableTypeInfo>();


        // Delegates that we use throughout
        public delegate Object FromBinaryFn(BinaryReader reader);
        public delegate void ToBinaryFn(BinaryWriter writer, Object value);
        public delegate String[] EnumerateOptionsFn();
        public delegate Object FromIndexFn(Int32 index);
        public delegate Int32 ToIndexFn(Object enumerableValue);

        /// <summary>
        /// Extra information about how to serialize a type.
        /// </summary>
        public class PrimitiveTypeInfo
        {
            public FromBinaryFn FromBinary;
            public ToBinaryFn ToBinary;
            public Type Type;
            public Int64 Guid;
        }

        public class EnumerableTypeInfo
        {
            public FromIndexFn FromIndex;
            public ToIndexFn ToIndex;
            public EnumerateOptionsFn Enumerate;
            public Type Type;
        }


        /// <summary>
        /// Static constructor that will initialize common types.
        /// </summary>
        static TypeFactory()
        {
            // Loop through all loaded assemblies
            foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
            {
                LoadTypesFromAssembly(assembly);
            }

            // Sign up for assembly load events
            AppDomain.CurrentDomain.AssemblyLoad += OnAssemblyLoad;

            // Register all the common types (primitives)
            RegisterCommonTypes();
        }

        private static void OnAssemblyLoad(object sender, AssemblyLoadEventArgs args)
        {
            LoadTypesFromAssembly(args.LoadedAssembly);
        }

        private static void LoadTypesFromAssembly(Assembly assembly)
        {
            // Loop through all the types in that assembly
            foreach (var currentType in assembly.GetTypes())
            {
                // Make a local copy of the type so that the closures below
                // refer to this up-value, instead of the enumerator (which gets changed)
                Type type = currentType;

                if (type.GetCustomAttributes(typeof(PropertyEnumAttribute), true).Length != 0)
                {
                    // Add the enum type to the map
                    RegisterEnumType(type);
                }
            }
        }

        /// <summary>
        /// Register a primitive type with the factory. Primitive types can be directly edited and serialized.
        /// </summary>
        public static void RegisterPrimitiveType(Type type, FromBinaryFn from, ToBinaryFn to)
        {
            RegisterPrimitiveTypeAs(type, type.Name, from, to);
        }

        /// <summary>
        /// Register a primitive type with the factory. Primitive types can be directly edited and serialized.
        /// </summary>
        public static void RegisterPrimitiveTypeAs(Type type, String name, FromBinaryFn from, ToBinaryFn to)
        {
            // If we do not already contain that type...
            if (TypeToPrimitiveInfo.ContainsKey(type) == false)
            {
                // Get the type's guid
                var guid = Extensions.HashString(name);

                // Create extra information about the primitive type
                var info = new PrimitiveTypeInfo()
                {
                    FromBinary = from,
                    ToBinary = to,
                    Type = type,
                    Guid = guid,
                };

                // Insert the type into both maps
                TypeToPrimitiveInfo.Add(type, info);
                GuidToPrimitiveInfo.Add(guid, info);
            }
            else
            {
                throw new Exception(String.Format("The type {0} was already registered", type));
            }
        }

        /// <summary>
        /// Register an enumerable type with the factory. Enumerable types have a list of enumerable human readable
        /// options, and an index that controls which option is currently selected. Things like enums or resources
        /// are typically bound as enumerable types.
        /// </summary>
        public static void RegisterEnumerableType(Type type, EnumerateOptionsFn enumerate, FromIndexFn from, ToIndexFn to)
        {
            // If we do not already contain that type...
            if (TypeToPrimitiveInfo.ContainsKey(type) == false)
            {
                // Create extra information about the primitive type
                var info = new EnumerableTypeInfo()
                {
                    FromIndex = from,
                    ToIndex = to,
                    Enumerate = enumerate,
                    Type = type,
                };

                // Insert the type into both maps
                TypeToEnumerableInfo.Add(type, info);
            }
            else
            {
                throw new Exception(String.Format("The type {0} was already registered", type));
            }
        }


        /// <summary>
        /// Add an enum type to the factory. Enum types are considered to be a special case of enumerable types.
        /// </summary>
        public static void RegisterEnumType(Type type)
        {
            // Make sure the given type is an enum type
            if (type.IsEnum == false)
            {
                throw new Exception("Attempted to register non-enum type as an enum in the TypeFactory");
            }

            // Make sure this is not a flags enum
            if (type.GetCustomAttributes(typeof(FlagsAttribute), true).Length != 0)
            {
                throw new Exception("You should not attempt to register flags enums as enumerable types in the TypeFactory");
            }

            // Register the enumerable type specially
            RegisterEnumerableType(type,
                () =>
                {
                    // Return all the names in the enum
                    return Enum.GetNames(type);
                },
                (index) =>
                {
                    // Next, get all the enumeration values
                    var enumeration = Enum.GetValues(type);

                    // Finally, return the enumeration value at the read index
                    return enumeration.GetValue(index);
                },
                (value) =>
                {
                    // Write the current index for the enum
                    var values = Enum.GetValues(type);

                    // Loop through all the values
                    for (Int32 i = 0; i < values.Length; ++i)
                    {
                        // Look for the first value we can find
                        if (value.Equals(values.GetValue(i)))
                        {
                            // We found the index for the given value!
                            return i;
                        }
                    }

                    // Throw an exception! (maybe it's bad data?)
                    throw new Exception("Unable to find enum value");
                });
        }


        /// <summary>
        /// Checks if a given type is primitive. This is typically used when building the property tree.
        /// </summary>
        public static Boolean IsPrimitive(Type type)
        {
            return TypeToPrimitiveInfo.ContainsKey(type);
        }

        /// <summary>
        /// Gets the extra type information froma the given type.
        /// </summary>
        public static PrimitiveTypeInfo GetInfoFromPrimitiveType(Type type)
        {
            return TypeToPrimitiveInfo[type];
        }

        /// <summary>
        /// Gets the extra type information froma the given type guid.
        /// </summary>
        public static PrimitiveTypeInfo GetInfoFromPrimitiveGuid(Int64 guid)
        {
            return GuidToPrimitiveInfo[guid];
        }

        /// <summary>
        /// Checks if a given type is enumerable. This is typically used when building the property tree.
        /// </summary>
        public static Boolean IsEnumerable(Type type)
        {
            return TypeToEnumerableInfo.ContainsKey(type);
        }

        /// <summary>
        /// Gets the extra type information froma the given type.
        /// </summary>
        public static EnumerableTypeInfo GetInfoFromEnumerableType(Type type)
        {
            return TypeToEnumerableInfo[type];
        }

        public static void WritePrimitiveDynamic(this BinaryWriter writer, Object value)
        {
            // Get the information about the type
            var typeInfo = TypeFactory.GetInfoFromPrimitiveType(value.GetType());

            // Write out the guid for the type
            writer.Write(typeInfo.Guid);

            // Serialize the type itself
            typeInfo.ToBinary(writer, value);
        }

        public static Object ReadPrimitiveDynamic(this BinaryReader reader)
        {
            // Read the type's guid
            var guid = reader.ReadInt64();

            // Get the information about the type by guid
            var typeInfo = TypeFactory.GetInfoFromPrimitiveGuid(guid);

            // Deserialize the type and return it
            return typeInfo.FromBinary(reader);
        }

        private static void RegisterCommonTypes()
        {
            RegisterPrimitiveType(typeof(Boolean),
                (reader) =>
                {
                    return reader.ReadBoolean();
                },

                (writer, value) =>
                {
                    writer.Write((Boolean)value);
                });

            RegisterPrimitiveType(typeof(Byte),
                (reader) =>
                {
                    return reader.ReadByte();
                },

                (writer, value) =>
                {
                    writer.Write((Byte)value);
                });

            RegisterPrimitiveType(typeof(SByte),
                (reader) =>
                {
                    return reader.ReadSByte();
                },

                (writer, value) =>
                {
                    writer.Write((SByte)value);
                });

            RegisterPrimitiveType(typeof(UInt16),
                (reader) =>
                {
                    return reader.ReadUInt16();
                },

                (writer, value) =>
                {
                    writer.Write((UInt16)value);
                });

            RegisterPrimitiveType(typeof(Int16),
                (reader) =>
                {
                    return reader.ReadInt16();
                },

                (writer, value) =>
                {
                    writer.Write((Int16)value);
                });

            RegisterPrimitiveType(typeof(UInt32),
                (reader) =>
                {
                    return reader.ReadUInt32();
                },

                (writer, value) =>
                {
                    writer.Write((UInt32)value);
                });

            RegisterPrimitiveType(typeof(Int32),
                (reader) =>
                {
                    return reader.ReadInt32();
                },

                (writer, value) =>
                {
                    writer.Write((Int32)value);
                });

            RegisterPrimitiveType(typeof(UInt64),
                (reader) =>
                {
                    return reader.ReadUInt64();
                },

                (writer, value) =>
                {
                    writer.Write((UInt64)value);
                });

            RegisterPrimitiveType(typeof(Int64),
                (reader) =>
                {
                    return reader.ReadInt64();
                },

                (writer, value) =>
                {
                    writer.Write((Int64)value);
                });

            RegisterPrimitiveType(typeof(Single),
                (reader) =>
                {
                    return reader.ReadSingle();
                },

                (writer, value) =>
                {
                    writer.Write((Single)value);
                });

            RegisterPrimitiveType(typeof(Double),
                (reader) =>
                {
                    return reader.ReadDouble();
                },

                (writer, value) =>
                {
                    writer.Write((Double)value);
                });

            RegisterPrimitiveType(typeof(String),
                (reader) =>
                {
                    return reader.ReadString();
                },

                (writer, value) =>
                {
                    writer.Write((String)value);
                });

            RegisterPrimitiveType(typeof(Char),
                (reader) =>
                {
                    return reader.ReadChar();
                },

                (writer, value) =>
                {
                    writer.Write((Char)value);
                });

            RegisterPrimitiveType(typeof(Vector2),
                (reader) =>
                {
                    return new Vector2(
                        reader.ReadSingle(),
                        reader.ReadSingle());
                },

                (writer, value) =>
                {
                    var val = (Vector2)value;
                    writer.Write(val.X);
                    writer.Write(val.Y);
                });

            RegisterPrimitiveType(typeof(Vector3),
                (reader) =>
                {
                    return new Vector3(
                        reader.ReadSingle(),
                        reader.ReadSingle(),
                        reader.ReadSingle());
                },

                (writer, value) =>
                {
                    var val = (Vector3)value;
                    writer.Write(val.X);
                    writer.Write(val.Y);
                    writer.Write(val.Z);
                });

            RegisterPrimitiveType(typeof(Vector4),
                (reader) =>
                {
                    return new Vector4(
                        reader.ReadSingle(),
                        reader.ReadSingle(),
                        reader.ReadSingle(),
                        reader.ReadSingle());
                },

                (writer, value) =>
                {
                    var val = (Vector4)value;
                    writer.Write(val.X);
                    writer.Write(val.Y);
                    writer.Write(val.Z);
                    writer.Write(val.W);
                });

            RegisterPrimitiveType(typeof(Quaternion),
                (reader) =>
                {
                    return new Quaternion(
                        reader.ReadSingle(),
                        reader.ReadSingle(),
                        reader.ReadSingle(),
                        reader.ReadSingle());
                },

                (writer, value) =>
                {
                    var val = (Quaternion)value;
                    writer.Write(val.X);
                    writer.Write(val.Y);
                    writer.Write(val.Z);
                    writer.Write(val.W);
                });

            RegisterPrimitiveType(typeof(Point),
                (reader) =>
                {
                    return new Point(
                        reader.ReadInt32(),
                        reader.ReadInt32());
                },

                (writer, value) =>
                {
                    var val = (Point)value;
                    writer.Write(val.X);
                    writer.Write(val.Y); ;
                });

            RegisterPrimitiveType(typeof(Rectangle),
                (reader) =>
                {
                    return new Rectangle(
                        reader.ReadInt32(),
                        reader.ReadInt32(),
                        reader.ReadInt32(),
                        reader.ReadInt32());
                },

                (writer, value) =>
                {
                    var val = (Rectangle)value;
                    writer.Write(val.X);
                    writer.Write(val.Y);
                    writer.Write(val.Width);
                    writer.Write(val.Height);
                });

            RegisterPrimitiveType(typeof(Color),
                (reader) =>
                {
                    return new Color(
                        reader.ReadByte(),
                        reader.ReadByte(),
                        reader.ReadByte(),
                        reader.ReadByte());
                },

                (writer, value) =>
                {
                    var val = (Color)value;
                    writer.Write(val.R);
                    writer.Write(val.G);
                    writer.Write(val.B);
                    writer.Write(val.A);
                });
        }
    }

    /// <summary>
    /// Declare that an enumeration is able to be used in bound properties.
    /// </summary>
    [AttributeUsage(AttributeTargets.Enum)]
    public class PropertyEnumAttribute : Attribute
    {
    }
}
